/*
 * Created on 28-Nov-2004
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

package org.gudy.azureus2.pluginsimpl.local.installer;

/**
 * @author parg
 *
 */

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.gudy.azureus2.core3.internat.MessageText;
import org.gudy.azureus2.core3.logging.LogAlert;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.core3.util.AERunnable;
import org.gudy.azureus2.core3.util.AESemaphore;
import org.gudy.azureus2.core3.util.AETemporaryFileHandler;
import org.gudy.azureus2.core3.util.AEThread2;
import org.gudy.azureus2.core3.util.AsyncDispatcher;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.FileUtil;
import org.gudy.azureus2.plugins.Plugin;
import org.gudy.azureus2.plugins.PluginEvent;
import org.gudy.azureus2.plugins.PluginException;
import org.gudy.azureus2.plugins.PluginInterface;
import org.gudy.azureus2.plugins.PluginManager;
import org.gudy.azureus2.plugins.installer.FilePluginInstaller;
import org.gudy.azureus2.plugins.installer.InstallablePlugin;
import org.gudy.azureus2.plugins.installer.PluginInstallationListener;
import org.gudy.azureus2.plugins.installer.PluginInstaller;
import org.gudy.azureus2.plugins.installer.PluginInstallerListener;
import org.gudy.azureus2.plugins.installer.StandardPlugin;
import org.gudy.azureus2.plugins.ui.UIManager;
import org.gudy.azureus2.plugins.ui.UIManagerEvent;
import org.gudy.azureus2.plugins.update.UpdatableComponent;
import org.gudy.azureus2.plugins.update.Update;
import org.gudy.azureus2.plugins.update.UpdateCheckInstance;
import org.gudy.azureus2.plugins.update.UpdateCheckInstanceListener;
import org.gudy.azureus2.plugins.update.UpdateChecker;
import org.gudy.azureus2.plugins.update.UpdateInstaller;
import org.gudy.azureus2.plugins.update.UpdateListener;
import org.gudy.azureus2.plugins.update.UpdateManager;
import org.gudy.azureus2.plugins.utils.StaticUtilities;
import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloader;
import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloaderAdapter;
import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloaderException;
import org.gudy.azureus2.pluginsimpl.local.FailedPlugin;
import org.gudy.azureus2.pluginsimpl.local.PluginInitializer;
import org.gudy.azureus2.pluginsimpl.local.update.UpdateCheckInstanceImpl;
import org.gudy.azureus2.pluginsimpl.local.update.UpdateManagerImpl;
import org.gudy.azureus2.pluginsimpl.update.PluginUpdatePlugin;
import org.gudy.azureus2.pluginsimpl.update.sf.SFPluginDetails;
import org.gudy.azureus2.pluginsimpl.update.sf.SFPluginDetailsException;
import org.gudy.azureus2.pluginsimpl.update.sf.SFPluginDetailsLoader;
import org.gudy.azureus2.pluginsimpl.update.sf.SFPluginDetailsLoaderFactory;

import com.aelitis.azureus.core.util.CopyOnWriteList;
import com.aelitis.azureus.core.vuzefile.VuzeFile;
import com.aelitis.azureus.core.vuzefile.VuzeFileComponent;
import com.aelitis.azureus.core.vuzefile.VuzeFileHandler;
import com.aelitis.azureus.core.vuzefile.VuzeFileProcessor;

public class PluginInstallerImpl implements PluginInstaller {
    protected static PluginInstallerImpl singleton;

    public static synchronized PluginInstallerImpl getSingleton(PluginManager _manager) {
        if (singleton == null) {

            singleton = new PluginInstallerImpl(_manager);
        }

        return (singleton);
    }

    private PluginManager manager;

    private CopyOnWriteList<PluginInstallerListener> listeners = new CopyOnWriteList<PluginInstallerListener>();

    private AsyncDispatcher add_file_install_dispatcher;

    protected PluginInstallerImpl(PluginManager _manager) {
        manager = _manager;

        VuzeFileHandler.getSingleton().addProcessor(new VuzeFileProcessor() {
            public void process(VuzeFile[] files, int expected_types) {
                for (int i = 0; i < files.length; i++) {

                    VuzeFile vf = files[i];

                    VuzeFileComponent[] comps = vf.getComponents();

                    for (int j = 0; j < comps.length; j++) {

                        VuzeFileComponent comp = comps[j];

                        if (comp.getType() == VuzeFileComponent.COMP_TYPE_PLUGIN) {

                            try {
                                Map content = comp.getContent();

                                String id = new String((byte[]) content.get("id"), "UTF-8");
                                String version = new String((byte[]) content.get("version"), "UTF-8");
                                String suffix = ((Long) content.get("is_jar")).longValue() == 1 ? "jar" : "zip";

                                byte[] plugin_file = (byte[]) content.get("file");

                                File temp_dir = AETemporaryFileHandler.createTempDir();

                                File temp_file = new File(temp_dir, id + "_" + version + "." + suffix);

                                FileUtil.copyFile(new ByteArrayInputStream(plugin_file), temp_file);

                                FilePluginInstaller installer = installFromFile(temp_file);

                                addFileInstallOperation(installer);

                                comp.setProcessed();

                            } catch (Throwable e) {

                                Debug.printStackTrace(e);
                            }
                        }
                    }
                }
            }
        });
    }

    protected void addFileInstallOperation(final FilePluginInstaller installer) {
        synchronized (this) {

            if (add_file_install_dispatcher == null) {

                add_file_install_dispatcher = new AsyncDispatcher();
            }

            add_file_install_dispatcher.dispatch(new AERunnable() {
                public void runSupport() {
                    try {
                        final AESemaphore done_sem = new AESemaphore("PluginInstall:fio");

                        final UIManager ui_manager = StaticUtilities.getUIManager(120 * 1000);

                        new AEThread2("PluginInstall:fio", true) {
                            public void run() {
                                if (installer.isAlreadyInstalled()) {

                                    String details =
                                            MessageText.getString("fileplugininstall.duplicate.desc", new String[] { installer.getName(),
                                                    installer.getVersion() });

                                    ui_manager.showMessageBox("fileplugininstall.duplicate.title", "!" + details + "!", UIManagerEvent.MT_OK);

                                    done_sem.release();

                                } else {

                                    String details =
                                            MessageText.getString("fileplugininstall.install.desc", new String[] { installer.getName(),
                                                    installer.getVersion() });

                                    long res =
                                            ui_manager.showMessageBox("fileplugininstall.install.title", "!" + details + "!", UIManagerEvent.MT_YES
                                                    | UIManagerEvent.MT_NO);

                                    if (res == UIManagerEvent.MT_YES) {

                                        try {
                                            install(new InstallablePlugin[] { installer }, false, true, null, new PluginInstallationListener() {
                                                public void completed() {
                                                    done_sem.release();
                                                }

                                                public void cancelled() {
                                                    done_sem.release();
                                                }

                                                public void failed(PluginException e) {
                                                    done_sem.release();

                                                    Debug.out("Installation failed", e);
                                                }
                                            });

                                        } catch (Throwable e) {

                                            Debug.printStackTrace(e);

                                            done_sem.release();
                                        }
                                    } else if (res == UIManagerEvent.MT_NO) {

                                        done_sem.release();

                                    } else {

                                        Debug.out("Message box not handled");

                                        done_sem.release();
                                    }
                                }
                            }
                        }.start();

                        while (!done_sem.reserve(60 * 1000)) {

                            if (add_file_install_dispatcher.getQueueSize() > 0) {

                                Debug.out("File plugin install operation queued pending completion of previous");
                            }
                        }

                    } catch (Throwable e) {

                        Debug.printStackTrace(e);
                    }
                }
            });
        }
    }

    protected PluginManager getPluginManager() {
        return (manager);
    }

    public StandardPlugin[] getStandardPlugins()

    throws PluginException {
        try {
            SFPluginDetailsLoader loader = SFPluginDetailsLoaderFactory.getSingleton();

            SFPluginDetails[] details = loader.getPluginDetails();

            List res = new ArrayList();

            for (int i = 0; i < details.length; i++) {

                SFPluginDetails detail = details[i];

                String name = detail.getId();

                String version = "";

                if (Constants.isCVSVersion()) {

                    version = detail.getCVSVersion();
                }

                if (version == null || version.length() == 0 || !Character.isDigit(version.charAt(0))) {

                    version = detail.getVersion();

                } else {

                    // if cvs version and non-cvs version are the same then show the
                    // non-cvs version

                    String non_cvs_version = detail.getVersion();

                    if (version.equals(non_cvs_version + "_CVS")) {

                        version = non_cvs_version;
                    }
                }

                if (name.startsWith("azplatform") || name.equals("azupdater")) {

                    // skip built in ones we don't want to let user install directly
                    // not the cleanest of fixes, but it'll do for the moment

                } else if (version == null || version.length() == 0 || !Character.isDigit(version.charAt(0))) {

                    // dodgy version

                } else if (detail.getCategory().equalsIgnoreCase("hidden")) {

                    // not public

                } else {

                    res.add(new StandardPluginImpl(this, details[i], version));
                }
            }

            StandardPlugin[] res_a = new StandardPlugin[res.size()];

            res.toArray(res_a);

            return (res_a);

        } catch (SFPluginDetailsException e) {

            throw (new PluginException("Failed to load standard plugin details", e));
        }
    }

    public StandardPlugin getStandardPlugin(String id)

    throws PluginException {
        try {
            SFPluginDetailsLoader loader = SFPluginDetailsLoaderFactory.getSingleton();

            SFPluginDetails[] details = loader.getPluginDetails();

            for (int i = 0; i < details.length; i++) {

                SFPluginDetails detail = details[i];

                String name = detail.getId();

                if (name.equals(id)) {

                    String version = "";

                    if (Constants.isCVSVersion()) {

                        version = detail.getCVSVersion();
                    }

                    if (version == null || version.length() == 0 || !Character.isDigit(version.charAt(0))) {

                        version = detail.getVersion();

                    } else {

                        // if cvs version and non-cvs version are the same then show the
                        // non-cvs version

                        String non_cvs_version = detail.getVersion();

                        if (version.equals(non_cvs_version + "_CVS")) {

                            version = non_cvs_version;
                        }
                    }

                    if (name.startsWith("azplatform") || name.equals("azupdater")) {

                        // skip built in ones we don't want to let user install directly
                        // not the cleanest of fixes, but it'll do for the moment

                    } else if (version == null || version.length() == 0 || !Character.isDigit(version.charAt(0))) {

                        // dodgy version

                    } else {

                        return (new StandardPluginImpl(this, details[i], version));
                    }
                }
            }

            return (null);

        } catch (SFPluginDetailsException e) {

            throw (new PluginException("Failed to load standard plugin details", e));
        }
    }

    public FilePluginInstaller installFromFile(File file)

    throws PluginException {
        return (new FilePluginInstallerImpl(this, file));
    }

    public void install(InstallablePlugin installable_plugin, boolean shared)

    throws PluginException {
        install(new InstallablePlugin[] { installable_plugin }, shared);
    }

    public void install(InstallablePlugin[] plugins, boolean shared)

    throws PluginException {
        install(plugins, shared, false, null, null);
    }

    public UpdateCheckInstance install(InstallablePlugin[] plugins, boolean shared, Map<Integer, Object> properties,
            final PluginInstallationListener listener)

    throws PluginException {
        return (install(plugins, shared, false, properties, listener));
    }

    protected UpdateCheckInstance install(InstallablePlugin[] plugins, boolean shared, boolean low_noise, Map<Integer, Object> properties,
            final PluginInstallationListener listener)

    throws PluginException {
        PluginInterface pup_pi = manager.getPluginInterfaceByClass(PluginUpdatePlugin.class);

        if (pup_pi == null) {

            throw (new PluginException("Installation aborted, plugin-update plugin unavailable"));
        }

        if (!pup_pi.getPluginState().isOperational()) {

            throw (new PluginException("Installation aborted, plugin-update plugin not operational"));
        }

        PluginUpdatePlugin pup = (PluginUpdatePlugin) pup_pi.getPlugin();

        UpdateManagerImpl uman = (UpdateManagerImpl) manager.getDefaultPluginInterface().getUpdateManager();

        UpdateCheckInstanceImpl inst = uman.createEmptyUpdateCheckInstance(UpdateCheckInstance.UCI_INSTALL, "update.instance.install", low_noise);

        if (properties != null) {

            for (Map.Entry<Integer, Object> entry : properties.entrySet()) {

                inst.setProperty(entry.getKey(), entry.getValue());
            }
        }

        if (listener != null) {

            inst.addListener(new UpdateCheckInstanceListener() {
                public void cancelled(UpdateCheckInstance instance) {
                    listener.cancelled();
                }

                public void complete(UpdateCheckInstance instance) {
                    final Update[] updates = instance.getUpdates();

                    if (updates.length == 0) {

                        listener.failed(new PluginException("No updates were added during check process"));

                    } else {

                        for (int i = 0; i < updates.length; i++) {

                            updates[i].addListener(new UpdateListener() {
                                private boolean cancelled;

                                public void cancelled(Update update) {
                                    cancelled = true;

                                    check();
                                }

                                public void complete(Update update) {
                                    check();
                                }

                                protected void check() {
                                    Update failed_update = null;

                                    for (Update update : updates) {

                                        if (!update.isCancelled() && !update.isComplete()) {

                                            return;
                                        }

                                        if (!update.wasSuccessful()) {

                                            failed_update = update;
                                        }
                                    }

                                    if (cancelled) {

                                        listener.cancelled();

                                    } else {

                                        if (failed_update == null) {

                                            // flush plugin events through before reporting complete

                                            PluginInitializer.waitForPluginEvents();

                                            listener.completed();

                                        } else {

                                            listener.failed(new PluginException("Install of " + failed_update.getName() + " failed"));
                                        }
                                    }
                                }
                            });
                        }
                    }
                }
            });
        }

        try {

            for (int i = 0; i < plugins.length; i++) {

                InstallablePlugin plugin = plugins[i];

                final String plugin_id = plugin.getId();

                PluginInterface existing_plugin_interface = manager.getPluginInterfaceByID(plugin_id, false);

                Plugin existing_plugin = null;

                if (existing_plugin_interface != null) {

                    existing_plugin = existing_plugin_interface.getPlugin();

                    // try to check that the new version is higher than the old one!

                    String old_version = existing_plugin_interface.getPluginVersion();

                    if (old_version != null) {

                        int res = Constants.compareVersions(plugin.getVersion(), old_version);

                        if (res < 0) {

                            throw (new PluginException("A higher version (" + old_version + ") of Plugin '" + plugin_id + "' is already installed"));

                        } else if (res == 0) {

                            throw (new PluginException("Version (" + old_version + ") of Plugin '" + plugin_id + "' is already installed"));
                        }
                    }
                }

                String target_dir;

                if (shared) {

                    target_dir = FileUtil.getApplicationFile("plugins").toString();

                } else {

                    target_dir = FileUtil.getUserFile("plugins").toString();
                }

                target_dir += File.separator + plugin_id;

                // this may fail on Vista but it doesn't matter as we recover this later
                // on. So *don't* check for success here

                new File(target_dir).mkdir();

                if (existing_plugin == null) {

                    // create a dummy plugin at version 0.0 to trigger the "upgrade" to the new
                    // installed version

                    FailedPlugin dummy_plugin = new FailedPlugin(plugin_id, target_dir);

                    PluginManager.registerPlugin(dummy_plugin, plugin_id);

                    final PluginInterface dummy_plugin_interface = manager.getPluginInterfaceByID(plugin_id, false);

                    ((InstallablePluginImpl) plugin).addUpdate(inst, pup, dummy_plugin, dummy_plugin_interface);

                    inst.addListener(new UpdateCheckInstanceListener() {
                        public void cancelled(UpdateCheckInstance instance) {
                            try {

                                dummy_plugin_interface.getPluginState().unload();

                            } catch (Throwable e) {

                                Debug.out("Failed to unload plugin", e);
                            }
                        }

                        public void complete(UpdateCheckInstance instance) {
                            PluginInterface pi = manager.getPluginInterfaceByID(plugin_id, false);

                            if (pi != null && pi.getPlugin() instanceof FailedPlugin) {

                                try {
                                    pi.getPluginState().unload();

                                } catch (Throwable e) {

                                    Debug.out("Failed to unload plugin", e);
                                }
                            }

                        }
                    });
                } else {

                    ((InstallablePluginImpl) plugin).addUpdate(inst, pup, existing_plugin, existing_plugin_interface);
                }
            }

            inst.start();

            return (inst);

        } catch (Throwable e) {

            inst.cancel();

            if (e instanceof PluginException) {

                throw ((PluginException) e);
            } else {

                throw (new PluginException("Failed to create installer", e));
            }
        }
    }

    public void uninstall(InstallablePlugin standard_plugin)

    throws PluginException {
        PluginInterface pi = standard_plugin.getAlreadyInstalledPlugin();

        if (pi == null) {

            throw (new PluginException(" Plugin '" + standard_plugin.getId() + "' is not installed"));
        }

        pi.getPluginState().uninstall();
    }

    public void uninstall(final PluginInterface pi)

    throws PluginException {
        uninstall(new PluginInterface[] { pi });
    }

    public void uninstall(final PluginInterface[] pis)

    throws PluginException {
        uninstall(pis, null);
    }

    public void uninstall(final PluginInterface[] pis, final PluginInstallationListener listener_maybe_null)

    throws PluginException {
        uninstall(pis, listener_maybe_null, new HashMap<Integer, Object>());
    }

    public UpdateCheckInstance uninstall(final PluginInterface[] pis, final PluginInstallationListener listener_maybe_null,
            final Map<Integer, Object> properties)

    throws PluginException {
        properties.put(UpdateCheckInstance.PT_UNINSTALL_RESTART_REQUIRED, false);

        for (int i = 0; i < pis.length; i++) {

            PluginInterface pi = pis[i];

            if (pi.getPluginState().isMandatory()) {

                throw (new PluginException("Plugin '" + pi.getPluginID() + "' is mandatory, can't uninstall"));
            }

            if (pi.getPluginState().isBuiltIn()) {

                throw (new PluginException("Plugin '" + pi.getPluginID() + "' is built-in, can't uninstall"));
            }

            String plugin_dir = pi.getPluginDirectoryName();

            if (plugin_dir == null || !new File(plugin_dir).exists()) {

                throw (new PluginException("Plugin '" + pi.getPluginID() + "' is not loaded from the file system, can't uninstall"));
            }
        }

        try {
            UpdateManager uman = manager.getDefaultPluginInterface().getUpdateManager();

            UpdateCheckInstance inst = uman.createEmptyUpdateCheckInstance(UpdateCheckInstance.UCI_UNINSTALL, "update.instance.uninstall");

            final int[] rds_added = { 0 };

            final AESemaphore rd_waiter_sem = new AESemaphore("uninst:rd:wait");

            for (int i = 0; i < pis.length; i++) {

                final PluginInterface pi = pis[i];

                final String plugin_dir = pi.getPluginDirectoryName();

                inst.addUpdatableComponent(new UpdatableComponent() {
                    public String getName() {
                        return (pi.getPluginName());
                    }

                    public int getMaximumCheckTime() {
                        return (0);
                    }

                    public void checkForUpdate(final UpdateChecker checker) {
                        try {
                            ResourceDownloader rd =
                                    manager.getDefaultPluginInterface().getUtilities().getResourceDownloaderFactory().create(new File(plugin_dir));

                            // the plugin may have > 1 plugin interfaces, make the name up appropriately

                            String update_name = "";

                            PluginInterface[] ifs = manager.getPluginInterfaces();

                            Arrays.sort(ifs, new Comparator() {
                                public int compare(Object o1, Object o2) {
                                    return (((PluginInterface) o1).getPluginName().compareTo(((PluginInterface) o2).getPluginName()));
                                }
                            });

                            for (int i = 0; i < ifs.length; i++) {

                                if (ifs[i].getPluginID().equals(pi.getPluginID())) {

                                    update_name += (update_name.length() == 0 ? "" : ",") + ifs[i].getPluginName();
                                }
                            }

                            boolean unloadable = pi.getPluginState().isUnloadable();

                            if (!unloadable) {

                                properties.put(UpdateCheckInstance.PT_UNINSTALL_RESTART_REQUIRED, true);
                            }

                            final Update update =
                                    checker.addUpdate(update_name, new String[] { "Uninstall: " + plugin_dir }, pi.getPluginVersion(), rd,
                                            unloadable ? Update.RESTART_REQUIRED_NO : Update.RESTART_REQUIRED_YES);

                            synchronized (rds_added) {

                                rds_added[0]++;
                            }

                            rd.addListener(new ResourceDownloaderAdapter() {
                                public boolean completed(ResourceDownloader downloader, InputStream data) {
                                    try {
                                        try {
                                            if (pi.getPluginState().isUnloadable()) {

                                                pi.getPluginState().unload();

                                                if (!FileUtil.recursiveDelete(new File(plugin_dir))) {

                                                    update.setRestartRequired(Update.RESTART_REQUIRED_YES);

                                                    properties.put(UpdateCheckInstance.PT_UNINSTALL_RESTART_REQUIRED, true);

                                                    checker.reportProgress("Failed to remove plugin, restart will be required");
                                                }
                                            }

                                            UpdateInstaller installer = checker.createInstaller();

                                            installer.addRemoveAction(new File(plugin_dir).getCanonicalPath());

                                            update.complete(true);

                                            try {
                                                PluginInitializer.fireEvent(PluginEvent.PEV_PLUGIN_UNINSTALLED, pi.getPluginID());

                                            } catch (Throwable e) {

                                                Debug.out(e);
                                            }
                                        } catch (Throwable e) {

                                            update.complete(false);

                                            Debug.printStackTrace(e);

                                            Logger.log(new LogAlert(LogAlert.REPEATABLE, "Plugin uninstall failed", e));
                                        }

                                        // don't close the stream as we process it later

                                        return (true);

                                    } finally {

                                        rd_waiter_sem.release();
                                    }
                                }

                                public void failed(ResourceDownloader downloader, ResourceDownloaderException e) {
                                    try {
                                        update.complete(false);

                                        if (!downloader.isCancelled()) {

                                            Logger.log(new LogAlert(LogAlert.REPEATABLE, "Plugin uninstall failed", e));
                                        }
                                    } finally {

                                        rd_waiter_sem.release();
                                    }
                                }
                            });
                        } finally {

                            checker.completed();
                        }

                    }
                }, false);
            }

            if (listener_maybe_null != null) {

                inst.addListener(new UpdateCheckInstanceListener() {
                    public void cancelled(UpdateCheckInstance instance) {
                        listener_maybe_null.cancelled();
                    }

                    public void complete(UpdateCheckInstance instance) {
                        // needs to be async in the case of the caller of the uninstall needing access to the
                        // updatecheckinstance and this is a sync callback

                        new AEThread2("Uninstall:async") {
                            public void run() {
                                int wait_count;

                                synchronized (rds_added) {

                                    wait_count = rds_added[0];
                                }

                                for (int i = 0; i < wait_count; i++) {

                                    rd_waiter_sem.reserve();
                                }

                                listener_maybe_null.completed();
                            }
                        }.start();
                    }
                });
            }

            inst.start();

            return (inst);

        } catch (Throwable e) {

            PluginException pe;

            if (e instanceof PluginException) {

                pe = (PluginException) e;

            } else {

                pe = new PluginException("Uninstall failed", e);
            }

            if (listener_maybe_null != null) {

                listener_maybe_null.failed(pe);
            }

            throw (pe);
        }
    }

    protected PluginInterface getAlreadyInstalledPlugin(String id) {
        return (getPluginManager().getPluginInterfaceByID(id, false));
    }

    public void requestInstall(String reason, InstallablePlugin plugin)

    throws PluginException {
        for (PluginInstallerListener listener : listeners) {

            try {
                if (listener.installRequest(reason, plugin)) {

                    return;
                }
            } catch (Throwable e) {

                Debug.out(e);
            }
        }

        throw (new PluginException("No listeners registered to perform installation of '" + plugin.getName() + " (" + reason + ")"));
    }

    public void addListener(PluginInstallerListener l) {
        listeners.add(l);
    }

    public void removeListener(PluginInstallerListener l) {
        listeners.remove(l);
    }
}
